OS - Lab3实验报告

归档于2025年7月8日。

思考题

3.1

  • UVPT指示的是整个页表空间的起始位置。
  • 在页表自映射的设计下, e->env_pgdir[PDX(UVPT)]访问了页目录中,指向页目录自身的页目录项
  • 将这一页目录项赋值为PADDR(e->env_pgdir) | PTE_V,也就是在构建自映射,并让这一项有效,从而使得页目录可被正确访问。

3.2

TLDR:

  • data域用于传入进程控制块e的指针;调用来源在load_icode()
  • 其作用,是为了得知进程控制块对应的内存空间,从而让程序机器码加载进进程对应的内存空间
  • 没有当然是不行的,不然操作系统根本不知道程序的机器码加载到哪…

以下是本人的探索:

Pasted image 20250413195908.png

在开始之前,我们不妨先理一下调用关系:

  • 先调用load_icode(),作为加载程序机器码的ABI
  • 随后,load_icode()遍历ehdr中的所有段,调用elf_load_seg(),将段加载进进程控制块e的空间内;

随后,我们目光转向elf_load_seg()
Pasted image 20250413200250.png

可以看到,加载分成两步:

  • 加载机器码
  • 处理页对齐问题(ROUNDDOWN()
  • 加padding

正如指导书所言,将实际加载交给外部函数进行,可以让elf_load_seg()只关心段的加载,而不必担心页面相关的操作。

3.3

  • 虚拟地址未按页对齐;见offset = va - ROUNDDOWN()部分
  • 正常加载:见Step 1
  • 机器码大小小于段大小:加padding,见Step 2

3.4

当然是虚拟地址。

PC(Program Counter)本身就是一个虚拟地址,其会映射到内存的某一部分,那一部分存放着程序的机器码。

既然PC是虚拟地址,那么记录出错发生时PC的EPC自然也是虚拟地址了。

3.5

genex.S中。handle_int在其中直接实现。

但是,对于其他函数,与其说函数被实现,不如说,函数在genex.S内由一个宏,映射到了一个已经实现了的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
genex.S:
1 #include <asm/asm.h>
2 #include <stackframe.h>
3
4 .macro BUILD_HANDLER exception handler
5 NESTED(handle_\exception, TF_SIZE + 8, zero)
6 move a0, sp
7 addiu sp, sp, -8
8 jal \handler
9 addiu sp, sp, 8
10 j ret_from_exception
11 END(handle_\exception)
12 .endm
...
// handle_int定义
20 NESTED(handle_int, TF_SIZE, zero)
21 mfc0 t0, CP0_CAUSE
22 mfc0 t2, CP0_STATUS
23 and t0, t2
24 andi t1, t0, STATUS_IM7
25 bnez t1, timer_irq
26 timer_irq:
27 li a0, 0
28 j schedule
29 END(handle_int)
30
// 调用宏,映射函数
31 BUILD_HANDLER tlb do_tlb_refill
32
33 #if !defined(LAB) || LAB >= 4
34 BUILD_HANDLER mod do_tlb_mod
35 BUILD_HANDLER sys do_syscall
36 #endif
37
38 BUILD_HANDLER reserved do_reserved

handle_int之所以要单独实现,是因为其涉及CP0的操作,这一部分必须依靠汇编语言实现。

对于其它的,例如handle_tlb(),我们在上面可以看到,其被映射到了我们之前实现的函数do_tlb_refill

3.6

  • 在通过enc_gen_entry进入异常处理程序后,时钟中断被关闭
  • 异常处理程序(e.g. handle_int())期间,时钟中断也处在关闭状态
  • 在处理完成,调用ret_from_exception()时,eret指令会将EXL置低;此时UM已被设为1,故此后运行时钟中断,进入用户态

3.7

此处我们就以实验环境的MIPS与MOS系统为例。

在一个体系结构(e.g. MIPS)下,会有一组状态寄存器(MIPS中,是CP0.Count/Compare),分别记录自开始以来经过的时钟周期,以及一次时钟中断需要经过的时钟周期;

Count每经过一周期加一次;加到与Compare相等时,产生时钟中断,同时在CP0的相关寄存器中记录下中断信息。

产生时钟中断后,系统进入内核态,自CP0读取当前的中断信息;得知此时产生的是“定时中断”,遂进入对应的异常处理程序:这里就是我们的schedule()

schedule()将对我们的env_sche_list进行操纵,将当前的进程放入调度队列末尾,随后选取待调度进程;

随后,使用env_run(),用选中的待调度进程,替换掉当前正运行的进程,其中进行了上下文的保存与切换。

对于其它体系结构与操作系统,上面的整体思路也是不变的。

难点分析

本人在进行Lab3时,遇到的最大困难,是进程切换的调度过程,以及其中的函数调用关系。

我们进程调度的相关代码,是在sched.c中完成的。

但是,其中的schedule()其实只负责env_sched_list、以及进程切换的时间计数count的维护;实际的切换,是在env_run()中完成的。

若无法理清过程中的函数调用关系,在这里就很容易栽跟头。我一开始就犯了这么一个错误:在schedule()中,就将curenv赋值为即将被调度执行的进程块,导致后续出错。

Lab3中,加载ELF时,load_icode()有关的函数调用关系也有些复杂。但在阅读源码后,我感觉自己的理解清晰了许多。(还是要RTFSC啊,笑

实验体会

  • 注意到Lab3处的注释,较前两次给出的提示有所减少;这为我们真正理解实验内容,自行编写代码提出了要求。
  • 坑点不少,需要细心:如schedule()中如何处理无待调度进程的情况(panic()),env_alloc()处,需要判断无空闲进程块的情况,返回-E_NO_FREE_ENV;这一点注释中没有明显提示。

原创说明

本次实验报告为本人原创。

作者

LajiPZ

发布于

2025-07-08

更新于

2025-07-09

许可协议

评论